package server.net;

import game.GameInstance;
import game.Player;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import server.GameServer;
import server.net.protocol.ping.DisconnectTask;
import server.net.protocol.ping.PingTask;
import shared.net.protocol.PacketCreator;
import shared.util.Randomizer;

/**
 * This represents a Nim user's client. We store each client's game data/information 
 * here as well as doing some single-user game logic.
 *
 * @author Vincent Tsuei
 * @version 11-13-2012
 */
public class Client {
    // Server object
    private GameServer server;
    
    //Low level IO
    private ClientConnection connection;
    
    //Login state information
    private LoginState loginState = LoginState.NOT_LOGGED_IN;
    
    //Player information
    private Player player;
    
    //Locks and Concurrency
    private final ReentrantReadWriteLock pingLock;
    private final ReentrantReadWriteLock loginLock;
    
    //Ping-related fields
    private static final long MILLISECONDS_PER_PING = 30000;
    private static final long PING_TIMEOUT_MILLISECOND = 15000;
    private PingState pingState = PingState.INACTIVE;
    private long lastPongRecv = System.currentTimeMillis(), lastPingSent = 0;
    private int lastPingId;
    private ScheduledFuture pingTask;
    private ScheduledFuture disconnectTask;
    
    /**
     * Create a client given a connection and a server object.
     */
    public Client(GameServer server, ClientConnection c) {
        this.server = server;
        this.connection = c;
        
        this.pingLock = new ReentrantReadWriteLock();
        this.pingTask = server.getScheduler().scheduleAtFixedRate(new PingTask(this), MILLISECONDS_PER_PING, MILLISECONDS_PER_PING, TimeUnit.MILLISECONDS);
        
        this.loginLock = new ReentrantReadWriteLock();
    }
    
    /**
     * Get the connection for this client.
     */
    public ClientConnection getConnection() {
        return connection;
    }
    
    /**
     * Returns whether or not this client is logged in.
     * Certain operations don't make sense (I.E. joining a game)
     * if the client is not logged in.
     */
    public boolean isLoggedIn() {
        loginLock.readLock().lock();
        try {
            return loginState == LoginState.LOGGED_IN;
        } finally {
            loginLock.readLock().unlock();
        }
    }
    
    
    /**
     * Returns whether the client is waiting for a pong.
     */
    public boolean isExpectingPong() {
        pingLock.readLock().lock();
        try {
            return pingState == PingState.WAITING_FOR_PONG;
        } finally {
            pingLock.readLock().unlock();
        }
    }    
    
    /**
     * Initiate a ping challenge to the client.
     */
    public void ping() {
        pingLock.writeLock().lock();
        try {
            if (pingState == PingState.WAITING_FOR_PONG) {
                //Already waiting for pong. Do nothing.
                return;
            }
            int pingId = Randomizer.getRandom().nextInt();
            connection.queuePacketSend(PacketCreator.PING.getPingPacket(pingId));
            disconnectTask = server.getScheduler().schedule(new DisconnectTask(this), PING_TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS);
            lastPingId = pingId;
            pingState = PingState.WAITING_FOR_PONG;
            lastPingSent = System.currentTimeMillis();
        } finally {
            pingLock.writeLock().unlock();
        }
    }
    
    /**
     * Let the client object know a pong was received now with the
     * specified pingId.
     * 
     * Silently ignore pongs that have an incorrect pingId.
     */
    public void pongReceived(int pingId) {
        pingLock.writeLock().lock();
        try {
            if (isExpectingPong() && lastPingId == pingId) {
                lastPongRecv = System.currentTimeMillis();
                pingState = PingState.INACTIVE;
                disconnectTask.cancel(false); //Don't cancel if in progress. Too late.
                disconnectTask = null;
            }
        } finally {
            pingLock.writeLock().unlock();
        }
    }
    
    /**
     * Disconnect the client.
     * This function will handle all higher level cleanup (e.g. remove player from registry)
     * Then, call NimConnection.shutdown() for low level network cleanup.
     */
    public void disconnect() {
        loginLock.writeLock().lock();
        try {
            //TODO. FINISH.
            if (isLoggedIn()) {
                logout();
            }

            //Cancel any ping/disconnect tasks.
            pingTask.cancel(false);
            if (disconnectTask != null && !disconnectTask.isDone())
                disconnectTask.cancel(false);

            //Shutdown the connection.
            connection.shutdown();
        } finally {
            loginLock.writeLock().unlock();
        }
    }
    
    /**
     * Return the player object associated with this client.
     * 
     * Guaranteed to return a player IFF isLoggedIn()
     * If !isLoggedIn(), returns null
     */
    public Player getPlayer()
    {
        return player;
    }
    
    /**
     * Tell this client to login with the specified player name.
     * Synchronize this method, it shouldn't be called very often, so this is okay.
     * 
     * The client must not already be logged in.
     * 
     * Returns 0 for success.
     * Less than 0 for failure:
     * ** -1 for already logged in.
     * ** -2 for name already taken.
     */
    public int login(String loginName) {
        loginLock.writeLock().lock();
        try {
            if (isLoggedIn()) {
                return -1; //Already logged in.
            }

            //if (player == null) { //Invalid state! We are in trouble. - Should never happen.
            //    throw new RuntimeException("login() - Player is null, but client is logged in.");
            //}

            Player p = getGameServer().getPlayerRegistry().addPlayer(loginName, this);
            if (p == null) {
                return -2;
            }
            this.player = p;
            this.loginState = LoginState.LOGGED_IN;
            return 0;
        } finally {
            loginLock.writeLock().unlock();
        }
    }
    
    /**
     * Tell the client to logout: release the player object.
     * Client must be logged in.
     * 
     * Returns 0 on success.
     * Less than 0 on failure:
     * ** -1 for not logged in.
     */
    public int logout() {
        System.out.println("Logging out: " + player.getName());
        loginLock.writeLock().lock();
            try {
            if (!isLoggedIn()) {
                return -1; //Not logged in.
            }

            if (player == null) { //Invalid state! We are in trouble. - Should never happen.
                throw new RuntimeException("logout() - Player is null, but client is logged in.");
            }

            //If player is in a game or observing the game, cleanup.
            if (player.isPlaying()) {
                GameInstance game = player.getGame();
                game.quitGame(player);
                getGameServer().getServerRegistry().deregisterGame(game);
            }
            if (player.isObserving()) {
               player.unobserveGame();
            }

            // Cleanup invitations.
            player.disableAndClearInvitations();
            
            // Deregister the player and cleanup.
            getGameServer().getPlayerRegistry().removePlayer(player);
            this.player = null;
            this.loginState = LoginState.NOT_LOGGED_IN;
            return 0;
        } finally {
            loginLock.writeLock().unlock();
        }
    }

    /**
     * Get the game server this client belongs to.
     */
    public GameServer getGameServer() {
        return server;
    }
    
}
